Skip to content

Epic: Rapier physics integration for Arcane clusters (Refs #8, #117, #118, #120, #121)#128

Open
martinjms wants to merge 12 commits intomainfrom
feat/rapier-cluster
Open

Epic: Rapier physics integration for Arcane clusters (Refs #8, #117, #118, #120, #121)#128
martinjms wants to merge 12 commits intomainfrom
feat/rapier-cluster

Conversation

@martinjms
Copy link
Copy Markdown
Contributor

Quick Summary

  • 🟡 Epic in progress. Living PR — accumulates as sub-PRs land into feat/rapier-cluster. Founder reviews + merges when the epic is complete.
  • Goal: authoritative server-side physics for Arcane clusters, via Rapier 0.32. First concrete physics backend named in #8.
  • Architectural cut: clean per-engine API (RapierClusterSimulation trait + RapierClusterTickContext), entity-keyed everything, integrates with the existing cluster_runner without changing networking / replication / persist.
  • What's in scope of this branch: the local Rapier integration (single-cluster physics) — RapierClusterSim, contact events, per-entity body kind / material / filtering / sensor hooks, in-tick imperative ops (apply impulse / force / torque, raycast, intersection queries, joints).
  • What's explicitly out of scope of this branch: cross-cluster physics interaction (kinematic proxies, imperative-op routing, atomic authority transfer) — tracked as its own epic at #127. Cross-cluster behavior on top of the primitives this branch ships.

Change Type

  • feature (epic)

Impact

  • User/developer impact: Game developers writing on Arcane can now declare authoritative physics behavior per-entity (Dynamic / Kinematic / Fixed bodies; per-entity friction / restitution / density; collision-group filtering; sensor volumes; collider shape). Inside on_tick they can apply forces and impulses, teleport, raycast, run intersection queries, and create joints — the full per-tick surface a local Rapier user has, all entity-keyed (no raw Rapier handles in user code).
  • Risk level: Medium. New public API surface in arcane-infra (a documented architectural pillar). All changes are scoped to the new rapier_cluster module + a small touch in cluster_runner for wiring; networking / replication / persist are unchanged. Wrapped in feature flag rapier-cluster (off by default for the existing benchmark binary).

Sub-PRs landed on feat/rapier-cluster

Sub-PR Sub-issue What landed
#123 #117 + #118 Minimum integration — RapierClusterSim wrapper, RapierClusterSimulation trait, contact events with one-tick delay, per-entity collider shapes (Ball / Capsule / Cuboid). Full review-driven polish + ~40 contract tests.
#125 #120 Per-entity body kind / material / filtering / sensor hooks. New types: RapierBodyKind { Dynamic / KinematicPositionBased / KinematicVelocityBased / Fixed }, RapierMaterial, RapierCollisionGroups. 8 new tests.
#126 #121 In-tick imperative physics ops via PhysicsHandle. apply_impulse / force / torque, set_translation / linvel / angvel, linvel / angvel reads, wake / sleep, raycast, intersections_with_shape, create_joint / remove_joint. Lock-window restructure for the Rapier-aware path. 11 new tests.

Architecture decisions captured (this branch)

Codified in ADR-001 (docs/architecture/adr/001-rapier-cluster-integration-shape.md) and the supporting docs added on this branch (entity-model.md, physics-backends-and-unreal.md updates, four-bucket-state-model.md updates):

  • Per-cluster simulation authority — no shared physics worlds across clusters; each cluster ticks its own. Cross-cluster coupling is its own epic (#127).
  • Entity-keyed invariant — every Rapier body is tied to an entity_id. No off-spine bodies; no raw Rapier handles in user code.
  • Per-engine API discipline — Rapier exposes Rust-native types. Unreal-Chaos parallel implementation tracked in #124.
  • Unified entity modelentity-model.md §4–§8 captures the canonical taxonomy (body kinds, subclass-vs-property-value polymorphism, affinity-bound vs spatial-bound binding, MapProvider for terrain).
  • Wire format unchanged — Rapier integration uses existing pose-spine + user_data buckets. No new replication channels for this branch.

Decisions made (aggregated from sub-PRs)

From #123 (Rapier minimum integration + contact events + colliders)
  • Wrap-don't-replace. RapierClusterSim wraps Option<Arc<dyn ClusterSimulation>> instead of replacing it. Same cluster_runner::run_cluster_loop powers both vanilla and Rapier clusters; networking / replication / neighbor merge / persist are guaranteed identical.
  • Position is output-only after first-sight spawn. First time an entity appears, its position seeds the body translation. Subsequent user writes are overwritten by Rapier's post-step output. Velocity remains intent-in.
  • Contact events delivered one tick delayed by design. User logic runs first to set intent → physics produces output → next tick's on_tick sees the contacts. Same-tick reactivity is a future follow-up.
  • Despawn does not surface a Stopped event. When a body is removed, contacts terminate silently; partner detects via the entity-map despawn.
  • Capsule built along the Y axis. Documented orientation; pinned by test.
  • Substep accumulator (1/60 s) over variable cluster tick. Stability + determinism.
From #125 (per-entity body kind / material / filtering / sensor hooks)
  • Bundle 5 spawn params into internal SpawnParams struct (vs 8-arg spawn() + #[allow(too_many_arguments)]). Cleaner site, gives one place to extend for future hooks; clippy-clean without lint suppression.
  • pub const fn new(...) constructors on RapierMaterial and RapierCollisionGroups (vs builder pattern). #[non_exhaustive] blocks struct-literal construction outside the defining crate; new is the simplest external constructor.
  • Re-export rapier3d::geometry::Group from arcane_infra root. Constructing collision groups requires Group::GROUP_1 etc.; re-exporting keeps rapier3d version pinning under arcane-infra's control.
  • RapierBodyKind derives Default with #[default] on Dynamic (vs manual impl). Idiomatic, shorter, satisfies clippy.
  • Restitution test tracks peak position only after tick 20 (vs across all ticks). Pre-impact peak is just the drop position — measuring post-impact is what distinguishes elastic vs inelastic.
  • Single parameterized HookSim test fixture (vs N narrow ones). 8 tests need heterogeneous per-entity configs + per-(hook, entity) call counting; one fixture covers all of them.
  • Test gravity setup pattern — set non-zero gravity per-test. Most existing tests rely on the default zero-gravity for benchmark parity; selectively overriding keeps the rest of the test suite unchanged.
From #126 (in-tick imperative ops via PhysicsHandle)
  • Lock held during user on_tick only for Backend::Rapier (vs all backends). Plain ClusterSimulation has no PhysicsHandle to give it; no functional reason to change its lock behavior.
  • pending_imperative_linvel: HashSet<Uuid> on RapierState (vs on PhysicsHandle). Symmetric with pending_contact_events; avoids lifetime gymnastics of extracting from PhysicsHandle.
  • Track only set_linvel/apply_impulse, not other ops. The spawn-loop sync only does set_linvel(entry.velocity) — no other op gets clobbered. Unnecessary tracking would just bloat the set.
  • QueryPipeline constructed transiently per-query (vs stored on RapierState as the issue spec wrote). Rapier 0.32 changed QueryPipeline to a borrowed view (QueryPipeline<'a>) — can no longer be a stored field. BroadPhaseBvh::as_query_pipeline(...) builds it cheaply on demand. Functionally equivalent.
  • JointSpec carries axis as plain Vec3 (vs UnitVector / Unit<Vector>). Rapier 0.32 takes Vector directly for joint axes; we .normalize() defensively to handle non-unit input.
  • JointId(ImpulseJointHandle) opaque newtype with private field. Preserves the entity-keyed invariant — user never holds a raw Rapier handle. Copy + Eq + Hash for ergonomic storage.
  • #[non_exhaustive] on JointSpec and RaycastHit with pub const fn new(...) on RaycastHit. Same external-construction issue as Rapier physics: per-entity body kind + material + collision filtering + sensor hooks #120's types.
  • Imperative ops on Fixed bodies silently no-op (return false) (vs panic, vs let Rapier handle it). Gameplay code shouldn't have to query body kind before applying force.
  • ScriptedSim<F> test fixture parameterized by closure (vs per-test struct). 11 tests need different in-tick behaviors; closure-parameterized fixture drops boilerplate. Per-test structs reserved for tests needing richer state (joint despawn cleanup, Fixed-body sim).

Verification

What this branch does NOT deliver (called out so reviewers don't expect it)

  • Cross-cluster physics interaction — separate epic at #127 (kinematic proxies, imperative-op routing, atomic authority transfer). Required for the demo's most interesting behavior; not in this branch.
  • Terrain / world geometry collision — separate epic at #119 (per-engine MapProvider).
  • Genre-specific optimizations — deterministic-projectile fast path for shooters etc. — layered primitives, when concrete customer needs surface.
  • Compound colliders (multiple shapes per body) — out of scope; deferred per #122.
  • Multibody joints (articulated chains) — out of scope; deferred per #122.
  • f64 Rapier feature — for far-from-origin precision past ~10⁴ units. Currently f32 internally; documented in module docs.

Reference

  • Parent EPIC: #8 — Cluster physics backends (Unreal Chaos + multi-engine path; this branch is the Rapier slice)
  • Sub-issues closed by this branch: #117 (minimum integration), #118 (contact events + colliders), #120 (body kind / material / filtering / sensor hooks), #121 (in-tick imperative ops)
  • Sub-PRs aggregated: #123, #125, #126
  • Inventory: #122 (Rapier gap inventory) — flips landed rows from 🚧 to ✅ once this merges
  • Sibling work (separate epics, not in this branch):
    • #119 — Terrain (per-engine MapProvider)
    • #124 — Unreal Cluster Node (Chaos parallel)
    • #127 — Cross-cluster physics interaction (newly filed; depends on this as foundation)
  • Downstream: brainy-bots/arcane-demos#6 — clustering visualization demo. Two of its three blocker dependencies are in this branch.
  • Architecture docs added on this branch:
    • docs/architecture/adr/001-rapier-cluster-integration-shape.md
    • docs/architecture/entity-model.md
    • docs/architecture/four-bucket-state-model.md (additions)
    • docs/architecture/physics-backends-and-unreal.md (additions)

martinjms and others added 12 commits May 2, 2026 08:58
…e physics (v1)

Introduces a feature-gated Rapier integration as a `ClusterSimulation` wrapper.
Drop-in for `run_cluster_loop`: same wire format, same networking, same
replication primitives — only the per-tick physics step is new.

Architecture:
- `RapierClusterSim` IS-A `ClusterSimulation` that HAS-A user `ClusterSimulation`.
  User logic runs first (intent / game actions), then Rapier integrates pose.
- `entity.velocity` is intent-in; `entity.position` is output-only after first-
  sight spawn (user position writes are silently overwritten by Rapier output).
- Despawn driven by `pending_removals` and entity-map disappearance; sync_inputs
  / sync_outputs filter pending-removal ids to avoid re-spawning bodies the
  user just asked to remove.
- Fixed 1/60 Rapier substeps with accumulator over the variable cluster tick.
- v1 default: uniform 0.5-radius sphere collider per entity; per-entity shapes
  via `user_data` schema deferred.

Feature gating:
- `rapier3d = "0.32"` declared `optional = true`; `rapier-cluster` feature
  pulls it in alongside `cluster-ws`.
- `arcane_rapier_cluster` binary requires `rapier-cluster`.
- Vanilla `cargo build -p arcane-infra` produces zero Rapier in the dep tree.

Tests:
- 18 unit tests covering lifecycle (spawn/despawn/respawn), multi-entity
  independence (incl. 500-entity scale), dynamics (velocity passthrough,
  gravity vs analytic kinematic, velocity-change-mid-sim), user-sim
  composition (correct context propagation, pending_removals from user code,
  buff-pattern velocity modulation), and determinism / despawn-respawn
  round-trip (hand-off scenario).
- Vanilla tests unchanged: 65 pass. Feature-on: 83 pass. Clippy silent both
  modes; doctest in module docs compiles.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…apierClusterSim

Adds the V2 surface so Rapier-backed nodes are usable for real games — without
this, integration is "uniform spheres with invisible collisions" (fine for tech
demo, useless for typical action gameplay).

Public additions:
- `RapierClusterSimulation` trait — sibling of `ClusterSimulation`, lives in
  `arcane-infra::rapier_cluster` so it can use Rapier types freely. Receives a
  `RapierClusterTickContext` instead of `ClusterTickContext`.
- `RapierClusterTickContext<'a>` — same fields as `ClusterTickContext` plus
  `contact_events: &[ContactEvent]` from the previous tick's physics step.
  One-tick delay by design: user logic runs first to set intent, physics produces
  output for next tick.
- `ContactEvent { entity_a, entity_b, started }` — collisions mapped from
  Rapier collider handles back to entity Uuids via a new reverse map.
- `RapierColliderShape::{Ball | Capsule | Cuboid}` — declared per entity via
  `RapierClusterSimulation::collider_for`. Default impl returns
  `Ball(config.default_body_radius)`. Resolved at first-sight spawn only;
  later returns are ignored (despawn-and-respawn to change shape).
- `RapierClusterSim::with_rapier_sim(rapier_sim, config)` constructor for the
  new trait. V1 `new` and `with_default_config` constructors preserved.

Internal refactor:
- `RapierClusterSim` now holds a private `Backend { None, Cluster, Rapier }` enum.
  `on_tick` dispatches per variant; the Rapier branch builds the extended ctx.
- `RapierState` gains `collider_to_entity: HashMap<ColliderHandle, Uuid>` for
  event mapping and `pending_contact_events: Vec<ContactEvent>` populated by a
  custom `EventHandler` impl (`CollisionRecorder`) installed during the step.
  Spawn loop and shape resolution moved out of `RapierState` into the wrapper
  so the active backend can drive `collider_for`.
- Every spawned collider sets `ActiveEvents::COLLISION_EVENTS`.

Tests:
- 6 new V2 tests: contact event surfaces for overlapping spheres; distant
  capsules produce no contacts; collider_for honored at first-sight spawn
  (verified via direct ColliderSet shape inspection); shape change after
  first-sight is ignored AND collider_for is called exactly once per entity;
  one-tick-delay semantics for contact events; no duplicate Started for a
  persistent overlap.
- All 18 V1 tests pass unchanged (same trait, same constructors, same wire).

Verification: 54 unit tests + 35 integration + 1 doctest pass under
`--features rapier-cluster`. Vanilla 65 tests pass; vanilla `cargo tree`
shows zero `rapier3d` references. Clippy silent both modes.

Closes #118.
Refs #8, #117.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ct tests

Synthesizes findings from the simplify skill (3 parallel review agents:
reuse / quality / efficiency) and a security-review pass, plus an
architectural pass on SemVer stability. Security review found zero
HIGH/MEDIUM vulnerabilities. Adds 14 tests so every module-doc claim is
backed by a test that would fail if the claim broke.

Code polish:
- New `to_rapier(Vec3) -> Vector` and `from_rapier(Vector) -> Vec3` helpers
  replacing five sites of triplet `.x as f32, .y as f32, .z as f32` casts.
- `RapierState::set_linvel` now takes `Vec3` instead of three `f64`s.
- Deleted unused `impl Default for RapierColliderShape` (closed a drift door
  vs `RapierConfig::default_body_radius`).
- `CollisionRecorder` propagates Mutex poison via `expect()` instead of
  silently dropping events on poison — surfaces panics that happen mid-step.
- Per-tick `pending_removals` lookup now uses a `HashSet` (was O(N×M) linear
  scan over a slice when entities × removals was non-trivial).
- Extracted `ClusterEnv::from_env()` helper into `cluster_runner`; both
  `arcane_cluster` and `arcane_rapier_cluster` binaries use it (was a
  verbatim duplicate of env-parsing across the two binaries).
- Stripped `v1`/`v2` release-stage labels from doc comments per project
  policy; trimmed several comments that restated the next line of code.

SemVer stability:
- `#[non_exhaustive]` on `RapierColliderShape`, `ContactEvent`,
  `RapierClusterTickContext`, `RapierConfig`. Future additions
  (e.g. `Cylinder` shape, `impulse_magnitude` on events, query handles in
  the context) won't be breaking changes.

New tests (every one corresponds to a documented contract that was
previously unverified):

T1 stopped_event_surfaces_when_bodies_separate
T2 despawn_during_contact_does_not_surface_stopped_event (pins the
   no-Stopped-on-despawn behavior; partners detect via the entity map)
T3 default_path_collider_is_a_ball_with_config_radius (V1 default shape
   directly inspected; previously only Cuboid was)
T4 capsule_collider_is_honored_at_first_sight (capsule shape inspected)
T5 multi_substep_in_one_cluster_tick (dt=0.1 → 6 substeps, position ~0.1)
T6 slow_dt_accumulates_until_substep_fires (dt=0.005, fires after ~3-4
   ticks, position converges to dt_total*v)
T7 contact_resolution_applies_impulse_to_partner (B gets pushed when A
   collides with it — Rapier responds, doesn't just detect)
T8 collider_for_invoked_freshly_on_respawn (despawn-respawn-same-uuid
   triggers a fresh shape decision)
T9 rapier_ctx_propagates_game_actions_tick_and_dt (V2 parallel of the V1
   context-propagation test)
T10 rapier_user_can_request_removal_via_pending_removals (V2 parallel of
    the V1 removal test)
T11 mixed_shape_ball_vs_cuboid_produces_contact (cross-shape collision
    now exercised; all prior contact tests paired same-shape)
T12 nondefault_gravity_honored_on_arbitrary_axis (gravity isn't hardcoded
    to -Y somewhere)
T13 contact_events_do_not_carry_across_handoff (cluster B's first tick
    sees ctx.contact_events == &[], not cluster A's events)
T14 capsule_axis_is_y (segment endpoints at (0, ±half_height, 0))

Verification: 68 lib tests (was 54) + 35 integration + 1 doctest pass under
`--features rapier-cluster`. Vanilla 65 unchanged. Clippy silent both modes.
Vanilla dep tree still has zero `rapier3d`.

Refs #117, #118, #8.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… invariant; clarify body kinds in physics backends doc

New canonical doc `docs/architecture/entity-model.md` codifies the unified-
entity model decided in the 2026-05-03 architecture session:

- Arcane has one persistent-world concept: the Entity. Players, NPCs,
  projectiles, dropped items, structures, player-built walls — all are
  entities differentiated by per-entity hooks (body kind, collider, material,
  collision groups, sensor mode), not by separate types at the platform
  level. Matches modern engine practice (Unreal AActor, Unity GameObject,
  Bevy ECS, Godot Node).
- Two-axis classification (animate × moving/stationary) is described with
  industry-standard term cross-references.
- Physics body kinds (Dynamic / KinematicPositionBased / KinematicVelocityBased
  / Fixed) are documented with their per-tick cost and migration semantics.
- Affinity-bound vs spatial-bound distinction is called out as a clustering
  concern (not a physics concern); needs follow-up work in the clustering
  model so Fixed entities don't migrate by PGP affinity.
- Terrain is explicitly NOT entities — it's content loaded by the Arcane
  runtime based on entity positions. Cross-link to terrain epic #119.

Updates to `four-bucket-state-model.md`:
- Adds the "every entity has bucket-4 durable state" universal invariant up
  front. This is what makes recovery / migration work and what unifies
  ephemeral game objects with structural ones in a single concept.

Updates to `physics-backends-and-unreal.md` §6 (entity ↔ body mapping):
- Adds body-kind row (per-entity hook, default Dynamic).
- Adds explicit terrain-is-not-entities row with cross-link to #119.
- Notes Rapier's sleep mechanism + Fixed-body solver-skip preserve the
  "no entities → no simulation" invariant without needing a separate
  Structure concept.

Refs #117, #118, #119, #120, #121, #122, #8, #33.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…in lines

CI's `cargo fmt --check` flagged six sites in rapier_cluster.rs (V2 tests with
long-form `entities.insert(id, mk_entry(...))` calls and one chained
with_collider closure) plus one site in cluster_runner.rs (long-line
Uuid::parse_str with map_err). Pure formatting; no functional changes.
All 38 rapier_cluster tests pass; clippy silent both modes.

Refs #117, #118, #123.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ic + terrain MapProvider framing

Updates entity-model.md with the architectural decisions from the 2026-05-03
sessions on cross-engine support and terrain handling.

§7 Terrain — rewritten:
- Static / voxel / procedural terrain shapes all supported through one
  per-engine MapProvider interface.
- Game owns storage (object storage / SpacetimeDB voxel chunks / on-disk /
  procedural / hybrid) and authoring tool (engine editor / voxel editor /
  generator). Arcane owns the loading interface.
- Voxel terrain content lives in SpacetimeDB; static mesh content in
  object storage; map manifest small in SpacetimeDB.

§8 (new) Conceptual contract vs. per-engine API:
- User-facing APIs are engine-native per plugin (UE C++, Unity C#, Godot,
  Rapier Rust). Wire format, manager / replication protocols, durable state
  schema invariant, and conceptual vocabulary are shared. Physics-property
  enums (BodyKind, ColliderShape, Material) are NOT promoted to a shared
  arcane-core; each plugin uses engine-native equivalents.
- Reverses an earlier proposal to unify physics value types — that would
  produce four parallel re-implementations of the same enum across language
  boundaries with no benefit.

§9 (new) Engine plugin pattern:
- Engine-named base classes (AArcaneUnrealEntity / ArcaneUnityEntity /
  ArcaneGodotEntity) extending engine-native types.
- Per-engine cluster runtime, MapProvider, in-tick imperative ops.
- Wire-format byte-compatibility across all engines via shared protocol.

§10 (new) Cross-engine entity migration:
- Entities can migrate between cluster tiers running different engines.
- Devs write per-engine game logic for each tier they support.
- Migration is at cluster-process boundaries; durable state in SpacetimeDB
  is the lingua franca.
- Cross-engine consistency for game rules (damage formulas, drop tables)
  lives in SpacetimeDB reducers called from every engine plugin.
- No in-process engine switching; "the function that runs physics for this
  engine" is the entire cluster binary written in that engine's language.

Refs #117, #118, #119, #122, #123, #124.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…isting channel) + SpacetimeDB for durability

Corrects an earlier framing that proposed SpacetimeDB pub/sub for
cross-cluster voxel chunk synchronization. The right pattern follows the
existing entity-replication mechanism:

- Real-time replication between clusters → Redis pub/sub (existing channel).
- Durable transactional storage → SpacetimeDB (per-chunk durable row).
- State that needs both (voxel chunks, destructible-terrain edits, runtime
  modifications) → write to SpacetimeDB AND publish on Redis.

Voxel chunks and destructible-terrain modifications are conceptually just
another kind of cross-cluster game state that's both immediate and durable.
Same mechanism Arcane already uses for EntityStateDelta.

Updates entity-model.md §7 with the corrected Cross-cluster coordination
subsection and the corrected Where-things-live storage table.

Memory anchor: project_redis_vs_spacetimedb_split.md.

Refs #119, #124.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Codifies the architectural decisions made during the Rapier integration
work (#117, #118, PR #123) for posterity. Closes the open ADR item in
#8's acceptance criteria for the Rapier track.

Decisions documented:

- Composition over inheritance — RapierClusterSim IS-A ClusterSimulation
  that wraps a user ClusterSimulation (or RapierClusterSimulation). No
  new PhysicsBackend trait introduced.
- In-process Rust library — no sidecar process, no FFI. Cargo feature
  rapier-cluster gates the optional rapier3d dependency. Vanilla builds
  pull zero rapier3d into the dep tree.
- Single Mutex<RapierState> wrapper; user code never sees RigidBodySet
  directly. Entity-keyed in-tick ops only — no off-spine bodies.
- Per-entity hooks called once at first-sight spawn; collider shape /
  material / body kind / collision groups / sensor are spawn-time
  decisions, not per-tick.
- Velocity in / position out contract. User mutations to entity.position
  during on_tick are silently overwritten by Rapier's post-step output.
- Contact events surface with one-tick delay (intent before output).
  Despawn-during-contact does NOT surface Stopped to the partner —
  partners detect via the entity map.
- All public types are `#[non_exhaustive]` from day one.

Alternatives considered + rejected:
- Separate crate `arcane-physics-rapier` with new PhysicsBackend trait
  (rejected — Cargo feature flag achieves dependency isolation with
  less ceremony).
- Sidecar process running Rapier (rejected — IPC overhead destroys
  per-tick budget for an in-process library).
- Direct &mut RigidBodySet exposure to user code (rejected — off-spine
  bodies and cross-cluster joints would silently break replication
  invariants).
- Engine-neutral physics types in arcane-core shared across backends
  (rejected — language barriers force per-plugin re-implementations
  anyway; documented in entity-model.md §8).

Updates physics-backends-and-unreal.md §7 to point at the ADR rather
than the earlier "separate crate per backend" framing, which the Rapier
work refined.

Updates docs/architecture/adr/README.md with an index of ADRs, marking
ADR-001 as Accepted and ADR-002 (Unreal Cluster Node) as Pending per
#124.

Refs #8, #117, #118, #119, #122, #123, #124.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
# Conflicts:
#	crates/arcane-infra/Cargo.toml
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant